ONLogN Linearithmic Time Exercises
- O(n log n) - Linearithmic Time Complexity Exercises
- Overview
- Exercise 1: Merge Sort
- Exercise 2: Quick Sort
- Exercise 3: Heap Sort
- Exercise 4: Sort Array by Frequency
- Exercise 5: Merge K Sorted Lists
- Exercise 6: Sort Colors (Dutch National Flag)
- Exercise 7: Meeting Rooms II
- Exercise 8: Top K Frequent Elements
- Exercise 9: Largest Number
- Exercise 10: Kth Largest Element
- Exercise 11: Sort List (Linked List)
- Exercise 12: Valid Anagram Using Sorting
- Exercise 13: Merge Intervals
- Exercise 14: Reorder Log Files
- Exercise 15: Binary Search on Sorted Array then Sort Result
- Exercise 16: Sort Characters by Frequency
- Exercise 17: Custom Sort String
- Exercise 18: Sort Array by Parity
- Exercise 19: Insert Interval
- Exercise 20: Closest Points to Origin
- Common Interview Questions
- Q1: "Why is merge sort O(n log n) and not O(n²)?"
- Q2: "Which sorting algorithm should I use in interviews?"
- Q3: "Is quick sort always better than merge sort?"
- Q4: "Can we sort faster than O(n log n)?"
- Q5: "When should I use sorting vs. a heap for 'top k' problems?"
- Summary
O(n log n) - Linearithmic Time Complexity Exercises
Overview
O(n log n) means the algorithm's runtime grows at a rate of n × log n. This complexity is typical of efficient sorting algorithms and algorithms that combine linear work with logarithmic divisions.
Key Characteristics:
- More expensive than O(n) but much better than O(n²)
- Best achievable for comparison-based sorting
- Common in divide-and-conquer algorithms
- Sweet spot for many practical algorithms
Growth Comparison:
n = 100 → n log n ≈ 664 operations
n = 1,000 → n log n ≈ 9,966 operations
n = 1,000,000 → n log n ≈ 19,931,569 operations
Compare to:
O(n) = 1,000,000
O(n²) = 1,000,000,000,000
Common Patterns:
- Sorting algorithms (Merge Sort, Quick Sort, Heap Sort)
- Divide-and-conquer with linear merge
- Building balanced trees
Exercise 1: Merge Sort
Problem: Sort an array using merge sort algorithm.
// Time: O(n log n) - Divides log n times, merges n elements each level
// Space: O(n) - Temporary arrays for merging
public class MergeSort
{
public void Sort(int[] arr)
{
if (arr.Length <= 1)
return;
MergeSortHelper(arr, 0, arr.Length - 1);
}
private void MergeSortHelper(int[] arr, int left, int right)
{
if (left >= right)
return;
int mid = left + (right - left) / 2;
// Divide: O(log n) levels
MergeSortHelper(arr, left, mid);
MergeSortHelper(arr, mid + 1, right);
// Conquer: O(n) work per level
Merge(arr, left, mid, right);
}
private void Merge(int[] arr, int left, int mid, int right)
{
int n1 = mid - left + 1;
int n2 = right - mid;
int[] leftArr = new int[n1];
int[] rightArr = new int[n2];
Array.Copy(arr, left, leftArr, 0, n1);
Array.Copy(arr, mid + 1, rightArr, 0, n2);
int i = 0, j = 0, k = left;
while (i < n1 && j < n2)
{
if (leftArr[i] <= rightArr[j])
{
arr[k] = leftArr[i];
i++;
}
else
{
arr[k] = rightArr[j];
j++;
}
k++;
}
while (i < n1)
{
arr[k] = leftArr[i];
i++;
k++;
}
while (j < n2)
{
arr[k] = rightArr[j];
j++;
k++;
}
}
}
Analysis:
- Divide: log n levels (halving each time)
- Conquer: O(n) work at each level (merging)
- Total: O(n log n)
- Space: O(n) for temporary arrays
- Stable: Yes (preserves relative order of equal elements)
- Best/Average/Worst: All O(n log n) - consistent performance!
Why O(n log n)?
Array size 8:
Level 0: [8] → 8 elements to merge
Level 1: [4][4] → 8 elements total to merge
Level 2: [2][2][2][2] → 8 elements total to merge
Level 3: [1][1][1][1][1][1][1][1] → base case
Levels: log₂(8) = 3
Work per level: 8 = n
Total: 3 × 8 = 24 = n log n
---
Exercise 2: Quick Sort
Problem: Sort an array using quick sort algorithm.
// Time: O(n log n) average, O(n²) worst case
// Space: O(log n) average for recursion stack
public class QuickSort
{
public void Sort(int[] arr)
{
QuickSortHelper(arr, 0, arr.Length - 1);
}
private void QuickSortHelper(int[] arr, int low, int high)
{
if (low < high)
{
int pivotIndex = Partition(arr, low, high);
QuickSortHelper(arr, low, pivotIndex - 1);
QuickSortHelper(arr, pivotIndex + 1, high);
}
}
private int Partition(int[] arr, int low, int high)
{
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++)
{
if (arr[j] <= pivot)
{
i++;
Swap(arr, i, j);
}
}
Swap(arr, i + 1, high);
return i + 1;
}
private void Swap(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// Randomized Quick Sort (better average case)
public class RandomizedQuickSort
{
private Random random = new Random();
public void Sort(int[] arr)
{
QuickSortHelper(arr, 0, arr.Length - 1);
}
private void QuickSortHelper(int[] arr, int low, int high)
{
if (low < high)
{
int pivotIndex = RandomizedPartition(arr, low, high);
QuickSortHelper(arr, low, pivotIndex - 1);
QuickSortHelper(arr, pivotIndex + 1, high);
}
}
private int RandomizedPartition(int[] arr, int low, int high)
{
int randomIndex = random.Next(low, high + 1);
Swap(arr, randomIndex, high);
return Partition(arr, low, high);
}
private int Partition(int[] arr, int low, int high)
{
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; j++)
{
if (arr[j] <= pivot)
{
i++;
Swap(arr, i, j);
}
}
Swap(arr, i + 1, high);
return i + 1;
}
private void Swap(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
Analysis:
- Best Case: O(n log n) - balanced partitions
- Average Case: O(n log n) - random pivots
- Worst Case: O(n²) - already sorted with bad pivot choice
- Space: O(log n) recursion stack (average)
- Stable: No
- In-Place: Yes (unlike merge sort)
Why Worst Case is O(n²)?
Sorted array [1, 2, 3, 4, 5] with last element as pivot:
Partition 1: n comparisons, pivot at end
Partition 2: n-1 comparisons, pivot at end
...
Total: n + (n-1) + (n-2) + ... + 1 = n(n+1)/2 = O(n²)
---
Exercise 3: Heap Sort
Problem: Sort using a heap data structure.
// Time: O(n log n) - Build heap + n deletions
// Space: O(1) - In-place (if we don't count recursion stack)
public class HeapSort
{
public void Sort(int[] arr)
{
int n = arr.Length;
// Build max heap: O(n)
for (int i = n / 2 - 1; i >= 0; i--)
{
Heapify(arr, n, i);
}
// Extract elements from heap: O(n log n)
for (int i = n - 1; i > 0; i--)
{
// Move current root to end
Swap(arr, 0, i);
// Heapify reduced heap: O(log n)
Heapify(arr, i, 0);
}
}
private void Heapify(int[] arr, int n, int i)
{
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest])
largest = left;
if (right < n && arr[right] > arr[largest])
largest = right;
if (largest != i)
{
Swap(arr, i, largest);
Heapify(arr, n, largest);
}
}
private void Swap(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
Analysis:
- Build Heap: O(n) - surprisingly not O(n log n)!
- Extract Max n times: n × O(log n) = O(n log n)
- Total: O(n) + O(n log n) = O(n log n)
- Best/Average/Worst: All O(n log n) - consistent!
- Space: O(1) - in-place
- Stable: No
---
Exercise 4: Sort Array by Frequency
Problem: Sort elements by their frequency of occurrence.
// Time: O(n log n) - Due to sorting
// Space: O(n) - Dictionary and sorted list
public int[] SortByFrequency(int[] nums)
{
// Count frequencies: O(n)
var freqMap = new Dictionary<int, int>();
foreach (int num in nums)
{
if (freqMap.ContainsKey(num))
freqMap[num]++;
else
freqMap[num] = 1;
}
// Sort by frequency: O(n log n)
var sorted = nums.OrderBy(x => freqMap[x])
.ThenBy(x => x)
.ToArray();
return sorted;
}
// Alternative using custom comparator
public int[] SortByFrequencyManual(int[] nums)
{
var freqMap = new Dictionary<int, int>();
foreach (int num in nums)
{
if (freqMap.ContainsKey(num))
freqMap[num]++;
else
freqMap[num] = 1;
}
Array.Sort(nums, (a, b) =>
{
int freqCompare = freqMap[a].CompareTo(freqMap[b]);
if (freqCompare != 0)
return freqCompare;
return a.CompareTo(b);
});
return nums;
}
Example:
Input: [1, 1, 2, 2, 2, 3]
Frequencies: 1→2, 2→3, 3→1
Output: [3, 1, 1, 2, 2, 2]
Analysis:
- Counting: O(n)
- Sorting: O(n log n)
- Total: O(n + n log n) = O(n log n)
---
Exercise 5: Merge K Sorted Lists
Problem: Merge k sorted linked lists into one sorted list.
public class ListNode
{
public int val;
public ListNode next;
public ListNode(int val = 0) { this.val = val; }
}
// Time: O(n log k) where n = total nodes, k = number of lists
// Space: O(k) - Priority queue size
public class MergeKSortedLists
{
public ListNode MergeKLists(ListNode[] lists)
{
if (lists == null || lists.Length == 0)
return null;
// Min heap (priority queue)
var pq = new SortedSet<(int val, int listIndex, ListNode node)>(
Comparer<(int val, int listIndex, ListNode node)>.Create((a, b) =>
{
int valCompare = a.val.CompareTo(b.val);
if (valCompare != 0) return valCompare;
return a.listIndex.CompareTo(b.listIndex);
})
);
// Add first node from each list: O(k log k)
for (int i = 0; i < lists.Length; i++)
{
if (lists[i] != null)
{
pq.Add((lists[i].val, i, lists[i]));
}
}
var dummy = new ListNode(0);
var current = dummy;
// Extract min and add next: O(n log k)
while (pq.Count > 0)
{
var min = pq.Min;
pq.Remove(min);
current.next = min.node;
current = current.next;
if (min.node.next != null)
{
pq.Add((min.node.next.val, min.listIndex, min.node.next));
}
}
return dummy.next;
}
}
// Divide and Conquer approach
public class MergeKSortedListsDivideConquer
{
public ListNode MergeKLists(ListNode[] lists)
{
if (lists == null || lists.Length == 0)
return null;
return MergeListsHelper(lists, 0, lists.Length - 1);
}
private ListNode MergeListsHelper(ListNode[] lists, int left, int right)
{
if (left == right)
return lists[left];
if (left > right)
return null;
int mid = left + (right - left) / 2;
ListNode leftList = MergeListsHelper(lists, left, mid);
ListNode rightList = MergeListsHelper(lists, mid + 1, right);
return MergeTwoLists(leftList, rightList);
}
private ListNode MergeTwoLists(ListNode l1, ListNode l2)
{
var dummy = new ListNode(0);
var current = dummy;
while (l1 != null && l2 != null)
{
if (l1.val <= l2.val)
{
current.next = l1;
l1 = l1.next;
}
else
{
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
current.next = l1 ?? l2;
return dummy.next;
}
}
Analysis:
- Priority Queue Approach: O(n log k) where n = total nodes
- Each of n nodes: inserted/removed from heap of size k
- Each operation: O(log k)
- Divide & Conquer: O(n log k)
- log k levels of merging
- O(n) work per level
---
Exercise 6: Sort Colors (Dutch National Flag)
Problem: Sort array with only 0s, 1s, and 2s.
// Time: O(n) - Single pass (not O(n log n), but worth mentioning)
// Space: O(1) - In-place
public void SortColors(int[] nums)
{
int low = 0, mid = 0, high = nums.Length - 1;
while (mid <= high)
{
if (nums[mid] == 0)
{
Swap(nums, low, mid);
low++;
mid++;
}
else if (nums[mid] == 1)
{
mid++;
}
else // nums[mid] == 2
{
Swap(nums, mid, high);
high--;
}
}
}
private void Swap(int[] nums, int i, int j)
{
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
// Compare to sorting: O(n log n)
public void SortColorsUsingSorting(int[] nums)
{
Array.Sort(nums); // O(n log n) - overkill for this problem!
}
Note: This is actually O(n), not O(n log n), but demonstrates when sorting is overkill. For limited value range, counting/bucketing can be better than general sorting.
---
Exercise 7: Meeting Rooms II
Problem: Find minimum number of conference rooms needed.
public class Interval
{
public int start;
public int end;
public Interval(int s, int e) { start = s; end = e; }
}
// Time: O(n log n) - Sorting intervals
// Space: O(n) - Priority queue
public int MinMeetingRooms(Interval[] intervals)
{
if (intervals.Length == 0)
return 0;
// Sort by start time: O(n log n)
Array.Sort(intervals, (a, b) => a.start.CompareTo(b.start));
// Min heap for end times
var endTimes = new SortedSet<(int endTime, int id)>();
int roomId = 0;
endTimes.Add((intervals[0].end, roomId++));
for (int i = 1; i < intervals.Length; i++)
{
var earliest = endTimes.Min;
// If earliest meeting ends before current starts, reuse room
if (earliest.endTime <= intervals[i].start)
{
endTimes.Remove(earliest);
}
endTimes.Add((intervals[i].end, roomId++));
}
return endTimes.Count;
}
// Alternative: Separate start/end arrays
public int MinMeetingRoomsAlternative(Interval[] intervals)
{
if (intervals.Length == 0)
return 0;
int[] starts = new int[intervals.Length];
int[] ends = new int[intervals.Length];
for (int i = 0; i < intervals.Length; i++)
{
starts[i] = intervals[i].start;
ends[i] = intervals[i].end;
}
Array.Sort(starts); // O(n log n)
Array.Sort(ends); // O(n log n)
int rooms = 0;
int maxRooms = 0;
int endPtr = 0;
for (int i = 0; i < starts.Length; i++)
{
if (starts[i] < ends[endPtr])
{
rooms++;
}
else
{
endPtr++;
}
maxRooms = Math.Max(maxRooms, rooms);
}
return maxRooms;
}
Analysis:
- Sorting: O(n log n)
- Processing: O(n log n) with heap operations
- Total: O(n log n)
---
Exercise 8: Top K Frequent Elements
Problem: Find k most frequent elements.
// Time: O(n log k) - Heap of size k
// Space: O(n) - Frequency map
public int[] TopKFrequent(int[] nums, int k)
{
// Count frequencies: O(n)
var freqMap = new Dictionary<int, int>();
foreach (int num in nums)
{
if (freqMap.ContainsKey(num))
freqMap[num]++;
else
freqMap[num] = 1;
}
// Min heap of size k: O(n log k)
var minHeap = new SortedSet<(int freq, int num)>();
foreach (var pair in freqMap)
{
minHeap.Add((pair.Value, pair.Key));
if (minHeap.Count > k)
{
minHeap.Remove(minHeap.Min);
}
}
return minHeap.Select(x => x.num).ToArray();
}
// Using sorting: O(n log n)
public int[] TopKFrequentSort(int[] nums, int k)
{
var freqMap = new Dictionary<int, int>();
foreach (int num in nums)
{
if (freqMap.ContainsKey(num))
freqMap[num]++;
else
freqMap[num] = 1;
}
return freqMap.OrderByDescending(x => x.Value)
.Take(k)
.Select(x => x.Key)
.ToArray();
}
Analysis:
- Heap approach: O(n log k) - better when k is small
- Sort approach: O(n log n) - simpler but slower
- Bucket sort approach exists: O(n) - best but more complex
---
Exercise 9: Largest Number
Problem: Arrange numbers to form the largest number.
// Time: O(n log n) - Custom sorting
// Space: O(n) - String array
public string LargestNumber(int[] nums)
{
// Convert to strings
string[] strs = nums.Select(x => x.ToString()).ToArray();
// Custom sort: O(n log n)
Array.Sort(strs, (a, b) =>
{
string order1 = a + b;
string order2 = b + a;
return order2.CompareTo(order1); // Descending
});
// Handle all zeros case
if (strs[0] == "0")
return "0";
return string.Join("", strs);
}
Example:
Input: [3, 30, 34, 5, 9]
Comparisons:
3 vs 30: "330" vs "303" → 3 > 30
9 vs 5: "95" vs "59" → 9 > 5
9 vs 34: "934" vs "349" → 9 > 34
Output: "9534330"
Analysis:
- Custom comparator determines optimal order
- Sorting: O(n log n)
- Each comparison: O(k) where k = average number length
- Total: O(n log n × k)
---
Exercise 10: Kth Largest Element
Problem: Find kth largest element in unsorted array.
// Using sorting: O(n log n)
public int FindKthLargestSort(int[] nums, int k)
{
Array.Sort(nums);
return nums[nums.Length - k];
}
// Using min heap: O(n log k) - Better!
public int FindKthLargestHeap(int[] nums, int k)
{
var minHeap = new SortedSet<(int val, int index)>();
for (int i = 0; i < nums.Length; i++)
{
minHeap.Add((nums[i], i));
if (minHeap.Count > k)
{
minHeap.Remove(minHeap.Min);
}
}
return minHeap.Min.val;
}
// Using QuickSelect: O(n) average, O(n²) worst - Best average case!
public int FindKthLargestQuickSelect(int[] nums, int k)
{
return QuickSelect(nums, 0, nums.Length - 1, nums.Length - k);
}
private int QuickSelect(int[] nums, int left, int right, int kSmallest)
{
if (left == right)
return nums[left];
int pivotIndex = Partition(nums, left, right);
if (kSmallest == pivotIndex)
return nums[kSmallest];
else if (kSmallest < pivotIndex)
return QuickSelect(nums, left, pivotIndex - 1, kSmallest);
else
return QuickSelect(nums, pivotIndex + 1, right, kSmallest);
}
private int Partition(int[] nums, int left, int right)
{
int pivot = nums[right];
int i = left;
for (int j = left; j < right; j++)
{
if (nums[j] <= pivot)
{
Swap(nums, i, j);
i++;
}
}
Swap(nums, i, right);
return i;
}
private void Swap(int[] nums, int i, int j)
{
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
Comparison:
- Sorting: O(n log n) time, O(1) space
- Min Heap: O(n log k) time, O(k) space
- QuickSelect: O(n) average, O(n²) worst, O(1) space
---
Exercise 11: Sort List (Linked List)
Problem: Sort a linked list using merge sort.
// Time: O(n log n) - Merge sort for linked list
// Space: O(log n) - Recursion stack
public ListNode SortList(ListNode head)
{
if (head == null || head.next == null)
return head;
// Find middle using slow/fast pointers
ListNode slow = head;
ListNode fast = head;
ListNode prev = null;
while (fast != null && fast.next != null)
{
prev = slow;
slow = slow.next;
fast = fast.next.next;
}
// Split into two halves
prev.next = null;
// Recursively sort both halves
ListNode left = SortList(head);
ListNode right = SortList(slow);
// Merge sorted halves
return Merge(left, right);
}
private ListNode Merge(ListNode l1, ListNode l2)
{
var dummy = new ListNode(0);
var current = dummy;
while (l1 != null && l2 != null)
{
if (l1.val <= l2.val)
{
current.next = l1;
l1 = l1.next;
}
else
{
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
current.next = l1 ?? l2;
return dummy.next;
}
Analysis:
- Can't use random access (no indexing in linked list)
- Merge sort is ideal for linked lists
- O(n log n) time, O(log n) space (recursion)
---
Exercise 12: Valid Anagram Using Sorting
Problem: Check if two strings are anagrams using sorting.
// Time: O(n log n) - Sorting both strings
// Space: O(n) - Character arrays
public bool IsAnagram(string s, string t)
{
if (s.Length != t.Length)
return false;
char[] sArr = s.ToCharArray();
char[] tArr = t.ToCharArray();
Array.Sort(sArr); // O(n log n)
Array.Sort(tArr); // O(n log n)
return new string(sArr) == new string(tArr);
}
Note: This is O(n log n), but counting approach is O(n). Sometimes sorting is easier to implement even if not optimal.
---
Exercise 13: Merge Intervals
Problem: Merge overlapping intervals.
// Time: O(n log n) - Sorting intervals
// Space: O(n) - Result list
public int[][] Merge(int[][] intervals)
{
if (intervals.Length <= 1)
return intervals;
// Sort by start time: O(n log n)
Array.Sort(intervals, (a, b) => a[0].CompareTo(b[0]));
var merged = new List<int[]>();
int[] current = intervals[0];
for (int i = 1; i < intervals.Length; i++)
{
if (intervals[i][0] <= current[1])
{
// Overlapping, merge
current[1] = Math.Max(current[1], intervals[i][1]);
}
else
{
// No overlap, add current and start new
merged.Add(current);
current = intervals[i];
}
}
merged.Add(current);
return merged.ToArray();
}
Example:
Input: [[1,3],[2,6],[8,10],[15,18]]
After sort: same
Merge [1,3] and [2,6] → [1,6]
Output: [[1,6],[8,10],[15,18]]
Analysis:
- Sorting: O(n log n)
- Merging: O(n)
- Total: O(n log n)
---
Exercise 14: Reorder Log Files
Problem: Reorder logs with letter-logs before digit-logs.
// Time: O(n log n) - Sorting with custom comparator
// Space: O(n) - Storage for sorted result
public string[] ReorderLogFiles(string[] logs)
{
return logs.OrderBy(log =>
{
var parts = log.Split(' ', 2);
return char.IsDigit(parts[1][0]) ? 1 : 0; // Digit logs last
})
.ThenBy(log =>
{
var parts = log.Split(' ', 2);
return char.IsDigit(parts[1][0]) ? "" : parts[1]; // Sort letter logs
})
.ThenBy(log =>
{
var parts = log.Split(' ', 2);
return char.IsDigit(parts[1][0]) ? "" : parts[0]; // Then by identifier
})
.ToArray();
}
// Manual sorting approach
public string[] ReorderLogFilesManual(string[] logs)
{
Array.Sort(logs, (log1, log2) =>
{
var parts1 = log1.Split(' ', 2);
var parts2 = log2.Split(' ', 2);
bool isDigit1 = char.IsDigit(parts1[1][0]);
bool isDigit2 = char.IsDigit(parts2[1][0]);
// Both letter logs
if (!isDigit1 && !isDigit2)
{
int comp = parts1[1].CompareTo(parts2[1]);
if (comp != 0) return comp;
return parts1[0].CompareTo(parts2[0]);
}
// One digit, one letter
if (!isDigit1 && isDigit2) return -1;
if (isDigit1 && !isDigit2) return 1;
// Both digit logs (maintain original order)
return 0;
});
return logs;
}
Analysis:
- Sorting with custom comparator: O(n log n)
- Each comparison: O(m) where m = average log length
- Total: O(n log n × m)
---
Exercise 15: Binary Search on Sorted Array then Sort Result
Problem: Find all elements in range then sort them.
// Time: O(log n + k log k) where k = elements in range
// Space: O(k) - Result array
public int[] FindAndSortInRange(int[] arr, int min, int max)
{
// Binary search for range start: O(log n)
int start = BinarySearchLeft(arr, min);
int end = BinarySearchRight(arr, max);
if (start > end)
return new int[0];
// Extract range: O(k)
int[] range = new int[end - start + 1];
Array.Copy(arr, start, range, 0, range.Length);
// Sort range: O(k log k)
Array.Sort(range);
return range;
}
private int BinarySearchLeft(int[] arr, int target)
{
int left = 0, right = arr.Length - 1;
int result = arr.Length;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (arr[mid] >= target)
{
result = mid;
right = mid - 1;
}
else
{
left = mid + 1;
}
}
return result;
}
private int BinarySearchRight(int[] arr, int target)
{
int left = 0, right = arr.Length - 1;
int result = -1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (arr[mid] <= target)
{
result = mid;
left = mid + 1;
}
else
{
right = mid - 1;
}
}
return result;
}
---
Exercise 16: Sort Characters by Frequency
Problem: Sort characters by frequency of occurrence.
// Time: O(n log n) - Sorting characters
// Space: O(n) - Frequency map and result
public string FrequencySort(string s)
{
// Count frequencies: O(n)
var freqMap = new Dictionary<char, int>();
foreach (char c in s)
{
if (freqMap.ContainsKey(c))
freqMap[c]++;
else
freqMap[c] = 1;
}
// Sort by frequency: O(n log n)
var sorted = freqMap.OrderByDescending(x => x.Value)
.ThenBy(x => x.Key);
// Build result: O(n)
var result = new StringBuilder();
foreach (var pair in sorted)
{
result.Append(pair.Key, pair.Value);
}
return result.ToString();
}
Example:
Input: "tree"
Frequencies: t→1, r→1, e→2
Output: "eert" or "eetr"
---
Exercise 17: Custom Sort String
Problem: Sort string based on custom order.
// Time: O(n log n) - Sorting with custom comparator
// Space: O(n) - Character array
public string CustomSortString(string order, string s)
{
// Create order map: O(m) where m = order.Length
var orderMap = new Dictionary<char, int>();
for (int i = 0; i < order.Length; i++)
{
orderMap[order[i]] = i;
}
// Sort string: O(n log n)
var chars = s.ToCharArray();
Array.Sort(chars, (a, b) =>
{
int orderA = orderMap.ContainsKey(a) ? orderMap[a] : int.MaxValue;
int orderB = orderMap.ContainsKey(b) ? orderMap[b] : int.MaxValue;
return orderA.CompareTo(orderB);
});
return new string(chars);
}
Example:
order = "cba"
s = "abcd"
Output: "cbad"
---
Exercise 18: Sort Array by Parity
Problem: Sort array so even numbers come before odd numbers.
// Using sorting: O(n log n)
public int[] SortArrayByParitySort(int[] nums)
{
Array.Sort(nums, (a, b) =>
{
return (a % 2).CompareTo(b % 2);
});
return nums;
}
// Two-pointer approach: O(n) - Better!
public int[] SortArrayByParityTwoPointer(int[] nums)
{
int left = 0, right = nums.Length - 1;
while (left < right)
{
if (nums[left] % 2 > nums[right] % 2)
{
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
if (nums[left] % 2 == 0) left++;
if (nums[right] % 2 == 1) right--;
}
return nums;
}
Note: Sorting is O(n log n) but two-pointer is O(n). Another case where sorting isn't optimal.
---
Exercise 19: Insert Interval
Problem: Insert new interval and merge if necessary.
// Time: O(n log n) if we sort, O(n) if already sorted
// Space: O(n) - Result list
public int[][] Insert(int[][] intervals, int[] newInterval)
{
var result = new List<int[]>();
int i = 0;
int n = intervals.Length;
// Add all intervals before newInterval
while (i < n && intervals[i][1] < newInterval[0])
{
result.Add(intervals[i]);
i++;
}
// Merge overlapping intervals
while (i < n && intervals[i][0] <= newInterval[1])
{
newInterval[0] = Math.Min(newInterval[0], intervals[i][0]);
newInterval[1] = Math.Max(newInterval[1], intervals[i][1]);
i++;
}
result.Add(newInterval);
// Add remaining intervals
while (i < n)
{
result.Add(intervals[i]);
i++;
}
return result.ToArray();
}
Analysis:
- If intervals are sorted: O(n)
- If need to sort first: O(n log n)
---
Exercise 20: Closest Points to Origin
Problem: Find k closest points to origin.
// Time: O(n log n) - Sorting all points
// Space: O(1) - Sort in place
public int[][] KClosest(int[][] points, int k)
{
Array.Sort(points, (a, b) =>
{
int distA = a[0] * a[0] + a[1] * a[1];
int distB = b[0] * b[0] + b[1] * b[1];
return distA.CompareTo(distB);
});
int[][] result = new int[k][];
Array.Copy(points, result, k);
return result;
}
// Using max heap: O(n log k) - Better when k is small!
public int[][] KClosestHeap(int[][] points, int k)
{
var maxHeap = new SortedSet<(int dist, int index)>(
Comparer<(int dist, int index)>.Create((a, b) =>
{
int distCompare = b.dist.CompareTo(a.dist);
if (distCompare != 0) return distCompare;
return b.index.CompareTo(a.index);
})
);
for (int i = 0; i < points.Length; i++)
{
int dist = points[i][0] * points[i][0] + points[i][1] * points[i][1];
maxHeap.Add((dist, i));
if (maxHeap.Count > k)
{
maxHeap.Remove(maxHeap.Min);
}
}
var result = new int[k][];
int idx = 0;
foreach (var item in maxHeap)
{
result[idx++] = points[item.index];
}
return result;
}
---
Common Interview Questions
Q1: "Why is merge sort O(n log n) and not O(n²)?"
Answer: Even though we have log n levels and do O(n) work per level, we're not doing n² comparisons. Each element is merged once per level. Total work = (log n levels) × (n work per level) = n log n.
Q2: "Which sorting algorithm should I use in interviews?"
Answer:
- Default: Mention you'd use built-in sort (O(n log n))
- Stable sort needed: Merge Sort
- In-place required: Quick Sort or Heap Sort
- Nearly sorted data: Insertion Sort can be better
- Small arrays: Insertion Sort
- Linked List: Merge Sort
Q3: "Is quick sort always better than merge sort?"
Answer: No. Quick sort has O(n²) worst case (already sorted array with bad pivot). Merge sort is always O(n log n). However, quick sort is often faster in practice due to better cache performance and in-place sorting. Use randomized quick sort to avoid worst case.
Q4: "Can we sort faster than O(n log n)?"
Answer: Not with comparison-based sorting. However, non-comparison sorts (Counting Sort, Radix Sort, Bucket Sort) can be O(n) for specific types of data.
Q5: "When should I use sorting vs. a heap for 'top k' problems?"
Answer:
- Full sort: O(n log n) - simple, works when k ≈ n
- Min/Max heap: O(n log k) - better when k << n
- QuickSelect: O(n) average - best average case, but O(n²) worst case
---
Summary
O(n log n) is the best we can do for comparison-based sorting. Key points:
- Main Pattern: Divide-and-conquer with linear merge
- Common Uses: Sorting, merging, divide-and-conquer algorithms
- Best Sorts: Merge Sort (stable, consistent), Quick Sort (in-place, fast average), Heap Sort (in-place, consistent)
- Optimal: For comparison-based sorting, O(n log n) is provably optimal
Next: Move on to ON2-Quadratic-Time-Exercises.md to learn about O(n²) complexity!